﻿Imports Framework.Data

Namespace FileSystemScan
    Friend Class Handler_ScanFolder
        Friend Delegate Function GroupFilesDelegate(ByVal share As Entities.Share, ByVal files As Generic.List(Of IO.FileInfo)) As Generic.Dictionary(Of String, ItemCandidate)
        Friend Delegate Function GetKeyForItemDelegate(ByVal share As Entities.Share, ByVal item As Entities.SharedItem) As String

        Friend Shared Sub Scan(ByVal share As Entities.Share, ByVal groupDelegate As GroupFilesDelegate, ByVal getKeyDelegate As GetKeyForItemDelegate, ByVal rootPath As String)
            FileShare.Engine.Manager.OnScanShareProgress(share, String.Format("Scanning share ""{0}"" for changes...", share.Name))

            Dim aFIs As New Generic.List(Of IO.FileInfo)
            'first, get all fileinfo's that reside in the share
            If String.IsNullOrEmpty(rootPath) Then
                For Each strPath As String In share.Paths
                    aFIs.AddRange(ScanFolder_Common.GetAllFileInfos(strPath, share.Recursive).Values.ToList)
                Next
            Else
                aFIs = ScanFolder_Common.GetAllFileInfosRecursive(rootPath).Values.ToList
            End If

            'now, we have all the files in the path(s) of the share
            'so, next step is to group them into items
            Dim aCandidates As Generic.Dictionary(Of String, ItemCandidate)
            aCandidates = groupDelegate.Invoke(share, aFIs)

            'get all existing items for this share and hash'em
            Dim aExisting As Generic.Dictionary(Of String, Entities.SharedItem) = GetAllShareItemsHashed(share, getKeyDelegate, rootPath)

            'now, remove all items that do not exist any more
            DeleteAndRemoveObsoleteItems(aCandidates, aExisting)

            'and, create all new items
            CreateNewItems(share, aCandidates, aExisting)

            'ok, now for the more complex part. Update existing items
            UpdateExistingItems(share, aCandidates, aExisting)

            FileShare.Engine.Manager.OnScanShareProgress(share, String.Format("Scanning of share ""{0}"" completed.", share.Name))
        End Sub

        Private Shared Sub UpdateExistingItems(ByVal share As Entities.Share, ByVal candidates As Generic.Dictionary(Of String, ItemCandidate), ByVal existing As Generic.Dictionary(Of String, Entities.SharedItem))
            For Each strKey As String In existing.Keys
                Dim clsItem As Entities.SharedItem = existing(strKey)
                Dim clsCand As ItemCandidate = candidates(strKey)

                SaveItemIfChangedAndAddDeleteOrUpdateSharedFiles(share, clsItem, clsCand)
            Next
        End Sub
        Private Shared Function AreEqual(ByVal candidate As ItemCandidate, ByVal item As Entities.SharedItem) As Boolean
            If item.Name <> candidate.Name Then Return False
            If item.Comment <> candidate.Comment Then Return False
            If item.Directory.ToLower <> candidate.DirectoryPath.ToLower Then Return False
            Return True
        End Function

        Private Shared Sub DeleteAndRemoveObsoleteItems(ByVal candidates As Generic.Dictionary(Of String, ItemCandidate), ByVal existing As Generic.Dictionary(Of String, Entities.SharedItem))
            For Each strKey As String In existing.Keys.ToArray
                If Not candidates.ContainsKey(strKey) Then
                    Dim clsItem As Entities.SharedItem = existing(strKey)
                    EntityHandlers.SharedItems.Delete(clsItem)
                    existing.Remove(strKey)
                End If
            Next
        End Sub
        Private Shared Sub CreateNewItems(ByVal share As Entities.Share, ByVal candidates As Generic.Dictionary(Of String, ItemCandidate), ByVal existing As Generic.Dictionary(Of String, Entities.SharedItem))
            For Each strKey As String In candidates.Keys.ToArray
                If Not existing.ContainsKey(strKey) Then
                    Dim clsCandidate As ItemCandidate = candidates(strKey)

                    Dim clsItem As New Entities.SharedItem
                    clsItem.Added = Now.ToUniversalTime
                    clsItem.Comment = clsCandidate.Comment
                    clsItem.Directory = clsCandidate.DirectoryPath
                    clsItem.GroupMode = share.GroupMode
                    clsItem.HasMediaInfo = False
                    clsItem.ItemType = share.ItemTypes
                    clsItem.LastChanged = Now.ToUniversalTime
                    clsItem.Name = clsCandidate.Name
                    clsItem.UniqueId = Framework.Misc.GenerateUniqueId
                    clsItem.ShareId = share.Id

                    SaveItemIfChangedAndAddDeleteOrUpdateSharedFiles(share, clsItem, clsCandidate)

                    candidates.Remove(strKey)
                End If
            Next
        End Sub

        Private Shared Sub SaveItemIfChangedAndAddDeleteOrUpdateSharedFiles(ByVal share As Entities.Share, ByVal item As Entities.SharedItem, ByVal candidate As ItemCandidate)
            Dim aFilesToSave As New Generic.List(Of Entities.SharedItemFile)

            Dim aExistingFiles As Generic.List(Of Entities.SharedItemFile)
            If item.Id = 0 Then
                aExistingFiles = New Generic.List(Of Entities.SharedItemFile)
            Else
                aExistingFiles = EntityHandlers.SharedItemsFiles.GetAllBySharedItemId(item.Id)
            End If
            Dim iTotalFiles As Integer = 0
            Dim lTotalSize As Long = 0
            For Each clsFI As IO.FileInfo In candidate.GetFiles
                iTotalFiles += 1
                lTotalSize += clsFI.Length

                'first, see if we already have this file
                Dim clsExisting As Entities.SharedItemFile = Nothing
                For Each clsTmp As Entities.SharedItemFile In aExistingFiles
                    If clsTmp.FileName.ToLower = clsFI.Name.ToLower Then
                        clsExisting = clsTmp
                        aExistingFiles.Remove(clsExisting)
                        Exit For
                    End If
                Next

                If HasFileChanged(clsFI, clsExisting) Then
                    If clsExisting Is Nothing Then clsExisting = New Entities.SharedItemFile
                    InitializeFile(clsExisting, item, clsFI)

                    aFilesToSave.Add(clsExisting)
                End If
            Next

            Dim blnAnyChanges As Boolean = False
            If iTotalFiles <> item.TotalFiles Then blnAnyChanges = True
            If lTotalSize <> item.TotalSize Then blnAnyChanges = True
            If aFilesToSave.Count > 0 Then blnAnyChanges = True
            If aExistingFiles.Count > 0 Then blnAnyChanges = True
            If item.Id = 0 Then blnAnyChanges = True
            If Not AreEqual(candidate, item) Then blnAnyChanges = True
            If share.ItemTypes <> item.ItemType Then blnAnyChanges = True
            If String.IsNullOrEmpty(item.RelativeDirectory) Then blnAnyChanges = True

            If blnAnyChanges Then
                item.TotalFiles = CShort(iTotalFiles)
                item.TotalSize = lTotalSize
                'see if name and/or comment changed
                item.Name = candidate.Name
                item.Comment = candidate.Comment
                item.ItemType = share.ItemTypes
                item.Directory = candidate.DirectoryPath
                Dim strRelDir As String = Nothing
                For Each strRoot As String In share.Paths
                    If item.Directory.ToLower.StartsWith(strRoot.ToLower) Then
                        strRelDir = item.Directory.Substring(strRoot.Length)
                        While strRelDir.StartsWith(IO.Path.DirectorySeparatorChar) Or strRelDir.StartsWith(IO.Path.AltDirectorySeparatorChar)
                            strRelDir = strRelDir.Substring(1)
                        End While
                    End If
                Next
                If strRelDir Is Nothing Then Throw New Exception("Invalid application state!")
                item.RelativeDirectory = strRelDir

                Using t As New TransactionScope
                    'start by saving item (to get the id if new...)
                    EntityHandlers.SharedItems.Save(item, t)

                    For Each clsFile As Entities.SharedItemFile In aFilesToSave
                        clsFile.SharedItemId = item.Id
                        EntityHandlers.SharedItemsFiles.Save(clsFile, t)
                        Using b As New FileSystemScan.RescanBlock(IO.Path.Combine(clsFile.Directory, clsFile.FileName))
                            Handler_ADS.UpdateADSMetaData(item, clsFile)
                        End Using
                    Next

                    'for all the items that remain in the exising list, "delete" from datastore
                    For Each clsFile As Entities.SharedItemFile In aExistingFiles
                        EntityHandlers.SharedItemsFiles.Delete(clsFile.Id, t)
                    Next

                    t.Complete()
                End Using

                'if any file changed - update mediainfo
                If aFilesToSave.Count > 0 Or aExistingFiles.Count > 0 Then
                    ScanFolder_Common.UpdateMediaInfoForExistingItem(item)
                End If
            Else
                'no files were changed - but do we need to update our media info table?
                ScanFolder_Common.EnsureMediaInfoIsUpToDate(item)
            End If
        End Sub

        Friend Shared Sub InitializeFile(ByVal file As Entities.SharedItemFile, ByVal item As Entities.SharedItem, ByVal fileInfo As IO.FileInfo)
            Dim strHashFirstMb As String = HashHelper.CalcSHA512ForFirstMb(fileInfo.FullName)
            Dim strHashLastMb As String = HashHelper.CalcSHA512ForLastMb(fileInfo.FullName)

            Dim clsMD As ADSMetaData = Handler_ADS.GetADSMetaDataIfValid(fileInfo)
            If file.Id = 0 Then 'new file
                file.Added = Now.ToUniversalTime
                'see if we can reuse an orphan
                Dim clsOrphan As Entities.SharedItemFile
                If clsMD IsNot Nothing Then
                    clsOrphan = EntityHandlers.SharedItemsFiles.FindOrphan(fileInfo.Length, clsMD.FileSHA512Hash)
                Else
                    clsOrphan = EntityHandlers.SharedItemsFiles.FindOrphan(fileInfo.Length, strHashFirstMb, strHashLastMb)
                End If
                If clsOrphan IsNot Nothing Then file.Id = clsOrphan.Id
            End If
            file.Directory = fileInfo.DirectoryName
            file.FileName = fileInfo.Name
            file.OrphanedSince = DateTime.MinValue
            file.SharedItemId = item.Id
            file.SHA512HashForFirstMb = strHashFirstMb
            file.SHA512HashForLastMb = strHashLastMb

            file.Size = fileInfo.Length
            file.LastChanged = fileInfo.LastWriteTimeUtc
            If clsMD IsNot Nothing Then
                file.SHA512Hash = clsMD.FileSHA512Hash
            Else
                'ok, no ADS metadata. Now, see if we can find info
                'in the database
                Dim strSha512 As String
                strSha512 = HashHelper.FindSHA512InDataStore(file.FileName, file.Size, file.SHA512HashForFirstMb, file.SHA512HashForLastMb)
                If strSha512 Is Nothing Then
                    'nope, unfortunately - we now need to calculate it!
                    'hint: we could ask our buddies if they know the hash...
                    strSha512 = HashHelper.CalcSHA512(fileInfo.FullName)
                End If
                file.SHA512Hash = strSha512
            End If
        End Sub

        Private Shared Function HasFileChanged(ByVal file As IO.FileInfo, ByVal sharedFile As Entities.SharedItemFile) As Boolean
            If sharedFile Is Nothing Then
                Return True
            ElseIf sharedFile.Size <> file.Length Then
                Return True
            ElseIf Math.Abs(sharedFile.LastChanged.Subtract(file.LastWriteTimeUtc).TotalSeconds) > 1 Then
                Return True
            ElseIf sharedFile.SHA512HashForFirstMb Is Nothing Or sharedFile.SHA512HashForLastMb Is Nothing Then
                Return True
            Else
                Return False
            End If
        End Function

        Private Shared Function GetAllShareItemsHashed(ByVal share As Entities.Share, ByVal getKeyDelegate As GetKeyForItemDelegate, ByVal rootPath As String) As Generic.Dictionary(Of String, Entities.SharedItem)
            Dim aItems As Generic.List(Of Entities.SharedItem)
            If String.IsNullOrEmpty(rootPath) Then
                aItems = EntityHandlers.SharedItems.GetAllByShareId(share.Id)
            Else
                aItems = EntityHandlers.SharedItems.GetAllByShareIdAndDirectoryRecursive(share.Id, rootPath)
            End If
            Dim aRet As New Generic.Dictionary(Of String, Entities.SharedItem)
            For Each clsItem As Entities.SharedItem In aItems
                Dim strKey As String = getKeyDelegate.Invoke(share, clsItem)
                If aRet.ContainsKey(strKey) Then
                    Beep()
                End If
                aRet.Add(strKey, clsItem)
            Next
            Return aRet
        End Function

        Friend Class ItemCandidate
            Public Key As String
            Public Name As String
            Public Comment As String
            Private mFiles As New Generic.List(Of IO.FileInfo)
            Private mTotalSize As Long = 0
            Private mDirecoryPath As String = Nothing

            Public ReadOnly Property TotalFiles() As Short
                Get
                    Return CShort(mFiles.Count)
                End Get
            End Property
            Public ReadOnly Property TotalSize() As Long
                Get
                    Return mTotalSize
                End Get
            End Property
            Public ReadOnly Property DirectoryPath() As String
                Get
                    Return mDirecoryPath
                End Get
            End Property
            Public Sub AddFile(ByVal file As IO.FileInfo)
                mFiles.Add(file)
                mTotalSize += file.Length
                If mDirecoryPath Is Nothing Then mDirecoryPath = file.DirectoryName
            End Sub
            Public Function GetFiles() As Generic.List(Of IO.FileInfo)
                Return mFiles
            End Function
        End Class
    End Class
End Namespace
